﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Concurrency;
using System.Threading;
using System.Collections.Concurrent;

namespace ReactiveCode
{
    class StateChangeScheduler
    {
        IScheduler _scheduler, _defaultScheduler;
        internal IScheduler BaseScheduler
        {
            get { return _scheduler; }
            set { _scheduler = value ?? _defaultScheduler; }
        }

        StateChangeScheduler()
        {
            Contract.Requires(SynchronizationContext.Current != null);
            _scheduler = _defaultScheduler = new SynchronizationContextScheduler(SynchronizationContext.Current);
        }

        [ThreadStatic]
        public static StateChangeScheduler _current;
        public static StateChangeScheduler Current
        {
            get { return _current ?? (_current = new StateChangeScheduler()); }
        }

        Dictionary<ReactiveStatement, Dictionary<object, Action>> _pendingNotifications = new Dictionary<ReactiveStatement, Dictionary<object, Action>>();

        public void ScheduleNotification(ReactiveStatement stmt, object stateIdentitiy, Action sendChange)
        {
            Contract.Requires(stmt != null && stateIdentitiy != null && sendChange != null);

            Debug.WriteLine("{0} Notification scheduled", DateTime.Now);

            lock(_pendingNotifications)
            {
                Dictionary<object, Action> notifs;
                if (!_pendingNotifications.TryGetValue(stmt, out notifs))
                    _pendingNotifications[stmt] = notifs = new Dictionary<object, Action>();
                notifs[stateIdentitiy] = sendChange;
                SendPendingNotifications(true);
            }
        }

        bool _isSendingScheduled;
        public void SendPendingNotifications(bool scheduled)
        {
            if (_pendingNotifications.Count == 0)
                return;

            if (scheduled)
            {
                if (_isSendingScheduled)
                    return;

                _isSendingScheduled = true;
                Action send = null;
                send = delegate
                {
                    try
                    {
                        SendPendingNotification();
                    }
                    catch
                    {
                        _isSendingScheduled = false;
                        throw;
                    }
                    if (_pendingNotifications.Count == 0)
                        _isSendingScheduled = false;
                    else
                        BaseScheduler.Schedule(send);
                };
                BaseScheduler.Schedule(send);
            }
            else
            {
                while (_pendingNotifications.Count > 0)
                    SendPendingNotification();
            }
        }
        void SendPendingNotification()
        {
            IEnumerable<Action> notifications;
            ReactiveStatement receiver;

            lock (_pendingNotifications)
            {
                var pair = _pendingNotifications.OrderBy(p => p.Key, ReactiveStatement.OrderComparer).FirstOrDefault();
                if (pair.Key == null)
                    return; // no pending notifications

                bool removed = _pendingNotifications.Remove(pair.Key);
                Contract.Assert(removed);
                receiver = pair.Key;
                notifications = pair.Value.Values;
            }

            try
            {
                foreach (var notif in notifications)
                    notif();
            }
            catch
            {
                Debug.WriteLine("Sending notifications caused an exception.\nReceiver: {0}", receiver);
                throw;
            }
            Debug.WriteLine("{0} Notification sent: {1}", DateTime.Now, receiver);
        }

        public void ScheduleChange(Action stateMutation)
        {
            Debug.WriteLine("{0} Change scheduled", DateTime.Now);
            BaseScheduler.Schedule(stateMutation);
        }
    }
}
